﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.ComponentModel;
using System.Collections;
using System.Threading;
using System.Linq.Expressions;
using System.Windows;

namespace ReactiveCode.Observables
{
    using Util;

    public static partial class Extensions
    {
        public static IObservable<T> IIf<T>(this IObservable<bool> test, IObservable<T> trueValue, IObservable<T> falseValue)
        {
            Contract.Requires(test != null && trueValue != null && falseValue != null);
            return test.SelectMany(x => x ? trueValue : falseValue);
        }

        internal static LatestValue<T> LatestContinuous<T>(this IObservable<T> source, bool distinct = true)
        {
            Contract.Requires(source != null);
            return new LatestValue<T>(source, distinct);
        }

        internal static IObservable<T[]> CombineLatest<T>(this IEnumerable<IObservable<T>> observables)
        {
            Contract.Requires(observables != null);

            var array = observables.ToArray();
            if (array.Contains(null))
                throw new ArgumentNullException();
            if (array.Length == 0)
                return Observable.Return(new T[0]);

            return Observable.Create<T[]>(o =>
            {
                T[] latestValues = new T[array.Length];
                bool hasAllValues = false;
                bool[] hasValues = new bool[array.Length];
                var subscriptions = new IDisposable[array.Length];

                for (int _i = 0; _i < array.Length; _i++)
                {
                    int i = _i;
                    subscriptions[i] = array[i].Subscribe(
                        next =>
                        {
                            hasValues[i] = true;
                            latestValues[i] = next;
                            if (!hasAllValues && hasValues.All(v => v))
                                hasAllValues = true;

                            if (hasAllValues)
                                o.OnNext(latestValues.ToArray());
                        },
                        o.OnError,
                        o.OnCompleted);
                }

                return delegate
                {
                    foreach (var s in subscriptions)
                        s.Dispose();
                };
            });
        }

        public static ISubject<TValue> PropertyOrField<TSource, TValue>(this IObservable<TSource> source, Expression<Func<TSource, TValue>> propertyOrField)
        {
            Contract.Requires(propertyOrField != null && propertyOrField.Body.NodeType == ExpressionType.MemberAccess);
            Contract.Requires(((propertyOrField.Body as MemberExpression).Member is FieldInfo) ||
                ((propertyOrField.Body as MemberExpression).Member is PropertyInfo && ((propertyOrField.Body as MemberExpression).Member as PropertyInfo).GetIndexParameters().Length == 0));

            var memberAccess = (MemberExpression)propertyOrField.Body;
            var setter = ReflectionUtil.ToSetter(memberAccess.Member);

            if (source == null && !memberAccess.Member.IsStatic())
                throw new ArgumentNullException("source");

            var listener = ChangeListening.CreateListener(memberAccess.Member);
            Func<TSource, IObservable<TValue>> observe = x => listener(x, null).DistinctUntilChanged().Cast<TValue>();

            return Subject.Create<TValue>
            (
                source == null ? observe(default(TSource)) : source.SelectMany(observe),
                newValue =>
                {
                    if (source == null)
                        setter(null, null, newValue);
                    else
                    {
                        var instance = source.Last();
                        if (instance != null)
                            setter(instance, null, newValue);
                    }
                }
            );
        }

        public static IDisposable Bind<T>(this ISubject<T> lhs, ISubject<T> rhs)
        {
            Contract.Requires(lhs != null && rhs != null);

            bool syncing = false;

            Func<ISubject<T>, ISubject<T>, IDisposable> bind =
                (a, b) => a.Subscribe(
                    onNext: x =>
                    {
                        if (syncing)
                            return;
                        syncing = true;
                        try
                        {
                            b.OnNext(x);
                        }
                        finally
                        {
                            syncing = false;
                        }
                    },
                    onError: rhs.OnError, onCompleted: rhs.OnCompleted);
            var s1 = bind(lhs, rhs);
            var s2 = bind(rhs, lhs);
            return Disposable.Create(delegate
            {
                s1.Dispose();
                s2.Dispose();
            });
        }

        internal static IObservable<T> SubscribeIf<T>(this IObservable<T> source, IObservable<bool> test)
        {
            Contract.Requires(source != null && test != null);
            return Observable.Create<T>(o =>
            {
                IDisposable srcSubscription = null;
                var testSubscription = test.Subscribe(
                    x =>
                    {
                        if (x)
                        {
                            if (srcSubscription == null)
                                srcSubscription = source.Subscribe(o.OnNext, o.OnError);
                        }
                        else
                        {
                            if (srcSubscription != null)
                            {
                                srcSubscription.Dispose();
                                srcSubscription = null;
                            }
                        }
                    
                    }, o.OnError);
                return delegate
                {
                    testSubscription.Dispose();
                    if (srcSubscription != null)
                        srcSubscription.Dispose();
                };

            });
        }

        public static ISubject<T> Decorate<T>(this ISubject<T> subject, IStateDecorator stateDecorator)
        {
            Contract.Requires(subject != null && stateDecorator != null);
            return stateDecorator.Apply(subject);
        }
        public static IObservable<T> Decorate<T>(this IObservable<T> observable, IStateDecorator stateDecorator)
        {
            Contract.Requires(observable != null && stateDecorator != null);
            return stateDecorator.Apply(observable);
        }

        internal static IObservable<T> Clone<T>(this IObservable<T> source)
        {
            Contract.Requires(source != null);
            return Observable.CreateWithDisposable<T>(source.Subscribe);
        }
        public static IObservable<Unit> AsUnit<T>(this IObservable<T> source)
        {
            Contract.Requires(source != null);
            return source.Select(_ => new Unit());
        }
        public static IObservable<Unit> CombineLatest(this IObservable<Unit> first, IObservable<Unit> second)
        {
            Contract.Requires(first != null && second != null);
            return first.CombineLatest(second, (x, y) => x);
        }
    }
}
